"use client"; /** * TASK-0133 — the ORG ADMIN console at `/org/[orgId]/admin`. * * Tabs (the phase-14 spec: overview, members, integrations, policies, * retention, API keys, webhooks, audit) + the P7 health tabs (T-176/177/178): * - overview → counts + 30-day storage/audit trend + effective policy * - members → list/search, role change, suspend/unsuspend * - integrations→ the EXISTING SsoTabs + ScimCard (TASK-0131/0132) * - policies → the five org policy toggles (enforced at 5 API points) * - retention → per-scope policy editor + dry-run / execute-now * - api keys → workspace picker + create/revoke (workspace-scoped API) * - webhooks → workspace picker + create/delete (workspace-scoped API) * - audit → filterable viewer + detail drawer + CSV export * - jobs → per-queue BullMQ state + failed jobs (DLQ) + requeue (T-176) * - search-health→ index vs source counts, drift, lag, last reconcile + reindex (T-177) * - notifications-health → in-app volume + email send/fail (T-178) * * Authorization: every backend route is `assertOrgAdmin`-gated server-side * (the API 403s a non-admin). The client-side gate here is UX only — a * non-admin still gets the data layer to reject them. The route itself is * session-gated (redirect to /login); the org-admin check rides on each API * call (a 403 surfaces the "you need org-admin rights" state). */ import { useEffect, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { ShieldCheck } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { orgAdminApi } from "@/lib/org-admin"; import { isApiError } from "@/lib/api"; import { cn } from "@/lib/utils"; import { SsoTabs } from "@/components/admin/SsoTabs"; import { ScimCard } from "@/components/admin/ScimCard"; import { AdminAuditTab } from "@/components/admin/org-admin/AuditTab"; import { AdminMembersTab } from "@/components/admin/org-admin/MembersTab"; import { AdminAnalyticsTab } from "@/components/admin/org-admin/AnalyticsTab"; import { AdminOverviewTab } from "@/components/admin/org-admin/OverviewTab"; import { AdminPoliciesTab } from "@/components/admin/org-admin/PoliciesTab"; import { AdminRetentionTab } from "@/components/admin/org-admin/RetentionTab"; import { AdminApiKeysTab } from "@/components/admin/org-admin/ApiKeysTab"; import { AdminWebhooksTab } from "@/components/admin/org-admin/WebhooksTab"; import { AdminJobsTab } from "@/components/admin/org-admin/JobsTab"; import { AdminSearchHealthTab } from "@/components/admin/org-admin/SearchHealthTab"; import { AdminNotificationsTab } from "@/components/admin/org-admin/NotificationsTab"; import { WorkspacePicker } from "@/components/admin/org-admin/WorkspacePicker"; const TABS = [ { id: "overview", label: "Overview" }, { id: "analytics", label: "Analytics" }, { id: "members", label: "Members" }, { id: "integrations", label: "Integrations" }, { id: "policies", label: "Policies" }, { id: "retention", label: "Retention" }, { id: "api-keys", label: "API keys" }, { id: "webhooks", label: "Webhooks" }, { id: "audit", label: "Audit" }, { id: "jobs", label: "Jobs" }, { id: "search-health", label: "Search health" }, { id: "notifications-health", label: "Notifications" }, ] as const; type TabId = (typeof TABS)[number]["id"]; function isTabId(v: string | null): v is TabId { return TABS.some((t) => t.id === v); } export default function OrgAdminConsolePage() { const { orgId } = useParams<{ orgId: string }>(); const router = useRouter(); const searchParams = useSearchParams(); const { user, initializing } = useAuth(); const [tab, setTab] = useState("overview"); useEffect(() => { if (!initializing && !user) { const next = window.location.pathname + window.location.search; router.replace(`/login?next=${encodeURIComponent(next)}`); } }, [initializing, user, router]); // Deep-link `?tab=…` (e.g. `/org/[orgId]/admin?tab=audit`). useEffect(() => { const t = searchParams.get("tab"); if (isTabId(t)) setTab(t); }, [searchParams]); const selectTab = (id: TabId) => { setTab(id); router.replace(`/org/${orgId}/admin?tab=${id}`, { scroll: false }); }; // The workspace picker is shared by the two WORKSPACE-scoped tabs (API // keys / webhooks are per-workspace in the API). Lifting the selection to // the page keeps one source of truth across both tabs. const [wsId, setWsId] = useState(null); const orgWorkspaces = useQuery({ queryKey: ["orgs", orgId, "admin", "workspaces"], queryFn: () => orgAdminApi.workspaces(String(orgId)), enabled: Boolean(orgId), retry: false, }); // Default the picker to the org's first workspace once it loads. useEffect(() => { const first = orgWorkspaces.data?.data[0]?.id; if (first && wsId === null) setWsId(first); }, [orgWorkspaces.data, wsId]); // The 403 gate: a non-org-admin gets no data at all. const forbidden = isApiError(orgWorkspaces.error) && orgWorkspaces.error.status === 403; if (initializing || !user) { return (
Loading…
); } const workspaces = orgWorkspaces.data?.data ?? []; return (
← Workspaces / Organization admin
{forbidden ? (
You need org-admin rights to open this console. Ask the organization owner to grant you the ADMIN role.
) : !orgId ? (

Missing org id.

) : ( <> {/* The workspace picker is shared by the two workspace-scoped tabs (API keys / webhooks are per-workspace in the API). */} {(tab === "api-keys" || tab === "webhooks") && ( )} {tab === "overview" && } {tab === "analytics" && } {tab === "members" && } {tab === "integrations" && (
)} {tab === "policies" && } {tab === "retention" && } {tab === "api-keys" && } {tab === "webhooks" && } {tab === "audit" && } {tab === "jobs" && } {tab === "search-health" && } {tab === "notifications-health" && } )}
); }